昨天把 Express 換成 TypeScript 版之後,今天繼續補型別功:泛型(Generics) 和 Utility Types。
這兩個主題讓型別更「可重用、可組合」,寫起來既靈活又安全。
直覺比喻:把型別做成參數。
就像函式的參數能決定行為,泛型參數能決定「這段程式操作的型別」。
wrapValue<T>
function wrapValue<T>(value: T): { value: T } {
return { value };
}
// 型別推論會自動帶入
const a = wrapValue(123); // { value: number }
const b = wrapValue("hello"); // { value: string }
const c = wrapValue({ id: 1 }); // { value: { id: number } }
有時候不想讓 T 隨便來,可以「約束」它:
// 只能接受「有 id 屬性」的物件
function withId<T extends { id: number }>(obj: T) {
return obj.id;
}
withId({ id: 1, name: "TS" }); // ✅ 1
// withId({ name: "no id" }); // ❌ 型別錯誤
function first<T>(list: T[]): T | undefined {
return list[0];
}
const n = first([1, 2, 3]); // n: number | undefined
const s = first(["a", "b"]); // s: string | undefined
先定義一個 Todo 型別,後面用它示範。
type Todo = {
id: number;
task: string;
done: boolean;
note?: string;
};
Partial<T>
:全部變成可選適合「更新」型的輸入(只改部分欄位)。
type TodoUpdate = Partial<Todo>;
/*
等同:
{
id?: number;
task?: string;
done?: boolean;
note?: string;
}
*/
function updateTodo(target: Todo, patch: TodoUpdate): Todo {
return { ...target, ...patch };
}
Pick<T, K>
:挑出想要的欄位適合 DTO 或 API 回應只列出必要欄位。
// 建立 Todo 的「建立輸入」型別,只需要 task/note
type CreateTodoDto = Pick<Todo, "task" | "note">;
const input: CreateTodoDto = { task: "寫 Day 22", note: "重點:泛型" };
// id/done 不在這個型別裡,所以不能亂塞
Omit<T, K>
:排除不要的欄位適合隱藏內部欄位(例如 id 是後端產的)。
// 用於「建立」時,前端不提供 id/done
type CreateTodoPayload = Omit<Todo, "id" | "done">;
// { task: string; note?: string }
Readonly<T>
:變成唯讀防止誤改資料(例如回傳值)。
function getTodo(): Readonly<Todo> {
return { id: 1, task: "學 TS", done: false };
}
const t = getTodo();
// t.done = true; // ❌ Readonly 禁止修改
type CreateTodoDto = Pick<Todo, "task" | "note">;
type TodoResponse = Readonly<Todo>;
function createTodo(dto: CreateTodoDto): TodoResponse {
const newTodo: Todo = {
id: Date.now(),
task: dto.task,
done: false,
note: dto.note,
};
return newTodo; // 自動套到 Readonly<Todo>
}
type UpdateTodoDto = Partial<Pick<Todo, "task" | "done" | "note">>;
function patchTodo(target: Todo, dto: UpdateTodoDto): Todo {
return { ...target, ...dto };
}
小心得:
Pick
搭Partial
很常見,因為「更新」通常只允許特定欄位,而且是選填。
在比較大的工具函式中,會用到 預設泛型:
// 預設把錯誤型別當作 string
type Result<TData, TError = string> = {
ok: boolean;
data?: TData;
error?: TError;
};
function ok<T>(data: T): Result<T> {
return { ok: true, data };
}
function fail<E = string>(error: E): Result<never, E> {
return { ok: false, error };
}
const r1 = ok({ id: 1 }); // Result<{id: number}, string>
const r2 = fail("Oops"); // Result<never, string>
const r3 = fail<{ code: number }>({ code: 500 }); // 自訂錯誤型別
wrapValue<T>(value: T): { value: T }
(上面已完成)
Pick
/ Omit
簡化 Todo 型別CreateTodoDto = Pick<Todo, "task" | "note">
CreateTodoPayload = Omit<Todo, "id" | "done">
UpdateTodoDto = Partial<Pick<Todo, "task" | "done" | "note">>
Partial
、Pick
、Omit
、Readonly
組一組,就能很快產出 DTO、更新輸入、唯讀輸出。